package impl.gui.util;

import java.awt.Component;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;

import javax.swing.BoundedRangeModel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.text.DefaultCaret;
import javax.swing.text.JTextComponent;

/**
 * The SmartScroller will attempt to keep the viewport positioned based on the
 * users interaction with the scrollbar. The normal behaviour is to keep the
 * viewport positioned to see new data as it is dynamically added.
 *
 * Assuming vertical scrolling and data is added to the bottom:
 *
 * - when the viewport is at the bottom and new data is added, then
 * automatically scroll the viewport to the bottom - when the viewport is not at
 * the bottom and new data is added, then do nothing with the viewport
 *
 * Assuming vertical scrolling and data is added to the top:
 *
 * - when the viewport is at the top and new data is added, then do nothing with
 * the viewport - when the viewport is not at the top and new data is added,
 * then adjust the viewport to the relative position it was at before the data
 * was added
 *
 * Similiar logic would apply for horizontal scrolling.
 */
public class SmartScroller implements AdjustmentListener {
	private final static int	HORIZONTAL		= 0;
	private final static int	VERTICAL		= 1;

	private final static int	START			= 0;
	private final static int	END				= 1;

	private int					viewportPosition;

	private JScrollBar			scrollBar;
	private boolean				adjustScrollBar	= true;

	private int					previousValue	= -1;
	private int					previousMaximum	= -1;

	/**
	 * Convenience constructor. Scroll direction is VERTICAL and viewport
	 * position is at the END.
	 *
	 * @param scrollPane
	 *            the scroll pane to monitor
	 */
	public SmartScroller(JScrollPane scrollPane) {
		this(scrollPane, VERTICAL, END);
	}

	/**
	 * Specify how the SmartScroller will function.
	 *
	 * @param scrollPane
	 *            the scroll pane to monitor
	 * @param scrollDirection
	 *            indicates which JScrollBar to monitor. Valid values are
	 *            HORIZONTAL and VERTICAL.
	 * @param viewportPosition
	 *            indicates where the viewport will normally be positioned as
	 *            data is added. Valid values are START and END
	 */
	@SuppressWarnings("ucd")
	public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition) {
		if (scrollDirection != HORIZONTAL && scrollDirection != VERTICAL)
			throw new IllegalArgumentException("invalid scroll direction specified");

		if (viewportPosition != START && viewportPosition != END)
			throw new IllegalArgumentException("invalid viewport position specified");

		this.viewportPosition = viewportPosition;

		if (scrollDirection == HORIZONTAL)
			scrollBar = scrollPane.getHorizontalScrollBar();
		else
			scrollBar = scrollPane.getVerticalScrollBar();

		scrollBar.addAdjustmentListener(this);

		// Turn off automatic scrolling for text components

		Component view = scrollPane.getViewport().getView();

		if (view instanceof JTextComponent) {
			JTextComponent textComponent = (JTextComponent) view;
			DefaultCaret caret = (DefaultCaret) textComponent.getCaret();
			caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
		}
	}

	@Override
	public void adjustmentValueChanged(final AdjustmentEvent e) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				checkScrollBar(e);
			}
		});
	}

	/*
	 * Analyze every adjustment event to determine when the viewport needs to be
	 * repositioned.
	 */
	private void checkScrollBar(AdjustmentEvent e) {
		// The scroll bar listModel contains information needed to determine
		// whether the viewport should be repositioned or not.

		JScrollBar scrollBar = (JScrollBar) e.getSource();
		BoundedRangeModel listModel = scrollBar.getModel();
		int value = listModel.getValue();
		int extent = listModel.getExtent();
		int maximum = listModel.getMaximum();

		boolean valueChanged = previousValue != value;
		boolean maximumChanged = previousMaximum != maximum;

		// Check if the user has manually repositioned the scrollbar

		if (valueChanged && !maximumChanged) {
			if (viewportPosition == START)
				adjustScrollBar = value != 0;
			else
				adjustScrollBar = value + extent >= maximum;
		}

		// Reset the "value" so we can reposition the viewport and
		// distinguish between a user scroll and a program scroll.
		// (ie. valueChanged will be false on a program scroll)

		if (adjustScrollBar && viewportPosition == END) {
			// Scroll the viewport to the end.
			scrollBar.removeAdjustmentListener(this);
			value = maximum - extent;
			scrollBar.setValue(value);
			scrollBar.addAdjustmentListener(this);
		}

		if (adjustScrollBar && viewportPosition == START) {
			// Keep the viewport at the same relative viewportPosition
			scrollBar.removeAdjustmentListener(this);
			value = value + maximum - previousMaximum;
			scrollBar.setValue(value);
			scrollBar.addAdjustmentListener(this);
		}

		previousValue = value;
		previousMaximum = maximum;
	}
}
